/* @flow */

import Dep from "./dep";
import VNode from "../vdom/vnode";
import { arrayMethods } from "./array";
import {
	def,
	warn,
	hasOwn,
	hasProto,
	isObject,
	isPlainObject,
	isPrimitive,
	isUndef,
	isValidArrayIndex,
	isServerRendering,
} from "../util/index";

const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

/**
 * In some cases we may want to disable observation inside a component's
 * update computation.
 */
export let shouldObserve: boolean = true;

export function toggleObserving(value: boolean) {
	shouldObserve = value;
}

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
	value: any;
	dep: Dep;
	vmCount: number; // number of vms that have this object as root $data

	constructor(value: any) {
		this.value = value;
		this.dep = new Dep();
		this.vmCount = 0;
		def(value, "__ob__", this);
		if (Array.isArray(value)) {
			if (hasProto) {
				protoAugment(value, arrayMethods);
			} else {
				copyAugment(value, arrayMethods, arrayKeys);
			}
			this.observeArray(value);
		} else {
			this.walk(value);
		}
	}

	/**
	 * Walk through all properties and convert them into
	 * getter/setters. This method should only be called when
	 * value type is Object.
	 */
	walk(obj: Object) {
		const keys = Object.keys(obj);
		for (let i = 0; i < keys.length; i++) {
			// 定义响应式
			defineReactive(obj, keys[i]);
		}
	}

	/**
	 * Observe a list of Array items.
	 */
	observeArray(items: Array<any>) {
		for (let i = 0, l = items.length; i < l; i++) {
			observe(items[i]);
		}
	}
}

// helpers

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment(target, src: Object) {
	/* eslint-disable no-proto */
	target.__proto__ = src;
	/* eslint-enable no-proto */
}

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment(target: Object, src: Object, keys: Array<string>) {
	for (let i = 0, l = keys.length; i < l; i++) {
		const key = keys[i];
		def(target, key, src[key]);
	}
}

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe(value: any, asRootData: ?boolean): Observer | void {
	if (!isObject(value) || value instanceof VNode) {
		return;
	}
	let ob: Observer | void;
	if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
		ob = value.__ob__;
	} else if (
		shouldObserve &&
		!isServerRendering() &&
		(Array.isArray(value) || isPlainObject(value)) &&
		Object.isExtensible(value) &&
		!value._isVue
	) {
		ob = new Observer(value);
	}
	if (asRootData && ob) {
		ob.vmCount++;
	}
	return ob;
}

/**
 * Define a reactive property on an Object.
 */
export function defineReactive(
	obj: Object,
	key: string,
	val: any,
	customSetter?: ?Function,
	shallow?: boolean
) {
	// 每一个响应式属性都有一个dep
	const dep = new Dep();

	// 如果属性的configurable为false，就不会做数据劫持，就不是响应式数据
	const property = Object.getOwnPropertyDescriptor(obj, key);
	if (property && property.configurable === false) {
		return;
	}

	// cater for pre-defined getter/setters
	const getter = property && property.get;
	const setter = property && property.set;
	if ((!getter || setter) && arguments.length === 2) {
		val = obj[key];
	}

	// 递归调用，为了确保所有数据都是响应式的
	let childOb = !shallow && observe(val);

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			const value = getter ? getter.call(obj) : val;
			if (Dep.target) {
				// 建立响应式联系
				dep.depend();
				if (childOb) {
					childOb.dep.depend();
					if (Array.isArray(value)) {
						dependArray(value);
					}
				}
			}
			return value;
		},
		set: function reactiveSetter(newVal) {
			const value = getter ? getter.call(obj) : val;
			/* eslint-disable no-self-compare */
			if (newVal === value || (newVal !== newVal && value !== value)) {
				return;
			}
			/* eslint-enable no-self-compare */
			if (process.env.NODE_ENV !== "production" && customSetter) {
				customSetter();
			}
			// #7981: for accessor properties without setter
			if (getter && !setter) return;
			if (setter) {
				setter.call(obj, newVal);
			} else {
				val = newVal;
			}
			// 确保新数据也是响应式
			childOb = !shallow && observe(newVal);
			// 通知watcher去更新视图
			dep.notify();
		},
	});
}

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set(target: Array<any> | Object, key: any, val: any): any {
	if (
		process.env.NODE_ENV !== "production" &&
		(isUndef(target) || isPrimitive(target))
	) {
		warn(
			`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
		);
	}
	if (Array.isArray(target) && isValidArrayIndex(key)) {
		target.length = Math.max(target.length, key);
		target.splice(key, 1, val);
		return val;
	}
	if (key in target && !(key in Object.prototype)) {
		target[key] = val;
		return val;
	}
	const ob = (target: any).__ob__;
	if (target._isVue || (ob && ob.vmCount)) {
		process.env.NODE_ENV !== "production" &&
			warn(
				"Avoid adding reactive properties to a Vue instance or its root $data " +
					"at runtime - declare it upfront in the data option."
			);
		return val;
	}
	if (!ob) {
		target[key] = val;
		return val;
	}
	defineReactive(ob.value, key, val);
	ob.dep.notify();
	return val;
}

/**
 * Delete a property and trigger change if necessary.
 */
export function del(target: Array<any> | Object, key: any) {
	if (
		process.env.NODE_ENV !== "production" &&
		(isUndef(target) || isPrimitive(target))
	) {
		warn(
			`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`
		);
	}
	if (Array.isArray(target) && isValidArrayIndex(key)) {
		target.splice(key, 1);
		return;
	}
	const ob = (target: any).__ob__;
	if (target._isVue || (ob && ob.vmCount)) {
		process.env.NODE_ENV !== "production" &&
			warn(
				"Avoid deleting properties on a Vue instance or its root $data " +
					"- just set it to null."
			);
		return;
	}
	if (!hasOwn(target, key)) {
		return;
	}
	delete target[key];
	if (!ob) {
		return;
	}
	ob.dep.notify();
}

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray(value: Array<any>) {
	for (let e, i = 0, l = value.length; i < l; i++) {
		e = value[i];
		e && e.__ob__ && e.__ob__.dep.depend();
		if (Array.isArray(e)) {
			dependArray(e);
		}
	}
}
